feat(packaging): AUR, Flathub, Scoop and Microsoft Store channels - #151
Merged
Conversation
The release already automated Homebrew (own tap), winget and signed apt/yum repos. What was missing were the channels users actually browse. - AUR: `voltius-bin` repackages the released .deb, pushed by a new job in publish-installers.yml. Skips with a warning when AUR_SSH_PRIVATE_KEY is unset so a release can never fail on it. - Flathub: manifest, AppStream metainfo and desktop file under packaging/flatpak. App id is app.voltius.Voltius, matching the domain Flathub verifies against, not the Tauri identifier. - Scoop and Flathub both carry checkver/x-checker-data, so their own bots follow releases after the first merge and need no job here. - Microsoft Store: AppxManifest template, build-msix.ps1 and a manual build-msix workflow. MSIX needs no code-signing certificate because the Store re-signs it; the .exe path would have required one. Three bugs found while packaging: - The .deb linked libsecret-1.so.0 without declaring it, so the app failed to start on a minimal Debian. Declared for deb and rpm. - The generated .desktop had an empty Categories, leaving the app uncategorised in every application menu. Set bundle.category. - An MSIX install lives under C:\Program Files\WindowsApps, which is not writable, so the updater would have downloaded and then failed at the install step. classify_install now reports that path as externally updated. Flatpak needed no change: no APPIMAGE env already means External. The four manifest generators shared one step - read the sha256 of a named release asset - so that moved to scripts/lib/release-assets.sh along with the asset naming. Tag resolution in publish-installers.yml was duplicated per job and is now a single job the others consume. README: dropped the stale star-count banner and the "coming soon" on a docs site that is live, added release/stars badges, and finished the comparison table's presentation - "(WIP)" gone, a legend added, and the blank and "NOT TESTED" cells normalised to "?".
The Store was the one channel left needing hands on every release. It does not: microsoft/microsoft-store-apppublisher sets up the Store Developer CLI, and `msstore publish` submits a package against a listing. build-msix.yml becomes publish-msix.yml, called from tag-release.yml alongside the other channels. It builds both architectures, bundles them with `makeappx bundle` (a listing serves one artifact per architecture from a single bundle, and `msstore publish` takes exactly one path), and submits. The submission step warns and skips unless all four Entra secrets and MSSTORE_PRODUCT_ID are set, so it can never fail a release. Two limits are inherent, not shortcuts, and are documented rather than worked around: - The CLI updates a listing, it cannot create one. The app must already be live in the Store, so the first submission stays manual. Running the workflow with `publish: false` produces the bundle for it. - Microsoft supports updates through this action for free products only. Voltius is free in the Store — Pro is billed outside it — so this holds, but a paid Store product would break it. Both build-msix.ps1 and the new bundle-msix.ps1 have to locate makeappx from the Windows SDK, so that moved to scripts/lib/makeappx.ps1.
Both manifests make claims that cannot be checked by reading them: that the Scoop install actually drives the NSIS installer and finds the binary afterwards, and that the cask passes `brew audit --new`, the gate a homebrew-cask core submission has to clear. Each needs an OS this project is not built on, and both are cheap on a hosted runner. verify-packaging.yml installs from the generated Scoop manifest on windows-latest and asserts the exe lands and uninstalls, runs the new-cask audit on macos-latest, and packs the MSIX (whose submission step is inert without Store credentials). It runs on changes to the packaging inputs so a manifest cannot rot silently between releases. The Scoop uninstaller now reads UninstallString from the uninstall registry key rather than assuming %LOCALAPPDATA%\Voltius\uninstall.exe, falling back to that path only when no entry matches.
`brew style` only applies the cask cops when the file sits under a Casks/ directory. The new-cask check styled a bare voltius.rb at the repo root, so rubocop fell back to generic Ruby cops and failed on Sorbet sigils and a missing frozen_string_literal comment — nothing to do with the cask. The release job already had the layout right, so rather than copy its generate/style/tap/audit block a second time with one flag changed, that block moves to a composite action both jobs call: the release audits the tap variant, the packaging check audits the core variant with --new. Also ignores the flatpak-builder output from a local test build, which a container writes into packaging/flatpak as root.
`brew audit --cask --new` — the gate a core submission has to pass — runs a signature scan, and it fails: Signature verification failed: Scan completed, but failed because the software is not signed by a distributor that meets the system Gatekeeper requirements. Voltius is ad-hoc signed, not notarized, so no amount of manifest work gets past this. `brew style` and the rest of the audit are clean, so notarization is the only thing in the way. Drops the core audit job rather than leaving a check that can only ever be red, and records the exact error so the next person does not rediscover it. The --core generator flag stays, ready for the day there is an Apple Developer account. The tap is unaffected and keeps being audited on release.
publish-msix used one global concurrency group, so a packaging check and a release submission were members of the same queue. With cancel-in-progress false, GitHub keeps only the newest *pending* entry and cancels the rest — three pushes in a row left the MSIX builds cancelled before they started. Keying the group on the ref separates them: a release runs against a tag, a check runs against a branch, and two runs of the same ref still serialize. verify-packaging also gets its own group with cancel-in-progress, so a new push supersedes the check rather than racing it. Its MSIX job is a full Windows Rust build on two targets, which is not something to leave running for a commit that has already been replaced.
The repository root is the Cargo workspace root, so builds land in ./target, not ./src-tauri/target. The pack step looked in the latter and threw after a thirty-minute Windows build had already succeeded. The error now lists the release binaries it did find. Getting a target triple or the target directory wrong is the likely cause, and rediscovering that costs another full build each time.
Rendered from src-tauri/icons/icon.png. The Store falls back to the icon inside the package when this is absent, but an uploaded tile takes priority and survives changes to the package icon set. Also records the listing asset rules, including that desktop screenshots are '1366x768 or larger' rather than one of two exact sizes — the existing 1600x1148 captures qualify unchanged.
packaging/msix/store-listing now holds every image the Partner Center listing form asks for: six screenshots numbered in upload order, each with the caption to go with it, plus the 300x300 app tile. Two of the screenshots were picked over the obvious candidates for a reason. The hosts view shows a single host, which sells "manage your fleet" badly, so the folders-and-tags view takes its place. And the dual-pane SFTP view leads, being the densest and cleanest of the set. Recorded in the directory's README: the split panes capture carries "tmux/screen not found - session will not survive disconnects" in three panes, because the host it was taken against had neither installed. Small, honest, and still a warning about a missing feature sitting in a shop window — worth recapturing, at which point it demonstrates persistent sessions instead of contradicting them.
Partner Center's Store logos section asks for poster art at exactly 720x1080 or 1440x2160 — no "or larger" tolerance, unlike screenshots. Microsoft's own documentation says this field "does not apply to apps" and is for games; the form asks regardless, so the form wins and both sizes are provided. The mark sits in the top two-thirds over a glow on the icon's own #010318, leaving the bottom third empty for the text the Store draws there. No wordmark: a font rendered in a container would not match the brand, and the bolt carries the identity on its own. scripts/make-store-poster.sh regenerates the poster and the app tile, running ImageMagick in a container so neither has to be installed on the host and neither ends up a hand-edited binary nobody can reproduce. The glow's outer stop is black on purpose, and the script says why: it is composited with 'screen', where black is a no-op, so the glow fades out invisibly instead of leaving the hard horizontal seam that an outer stop of #010318 produced.
makeappx refuses the package outright: error 80080204: App manifest validation error: The DefaultTile element must specify the Wide310x150Logo attribute if the Square310x310Logo attribute is specified. `tauri icon` emits only square logos, so the wide tile is generated alongside the Store listing art. It is transparent behind the mark, like the square logos, because the manifest sets BackgroundColor="transparent" and Windows paints the accent colour behind the tile. Dropping Square310x310Logo would also have satisfied makeappx, at the cost of the large Start tile. Producing the missing asset keeps both. make-store-poster.sh becomes make-store-assets.sh: it now emits a package asset as well as listing art, and the old name no longer describes it.
The MSIX version comes from tauri.conf.json, which only carries the released version at a release tag: on dev it lags, because the bump lands on main. The packaging check builds a branch, so its bundle came out labelled 0.25.0 while the latest release was 0.27.0 — fine for proving the package builds, and wrong to submit. That matters more than a mislabelled test artifact. Store versions only move forward, so a first submission at the wrong version cannot be taken back. The publish job now compares the packaged version against the latest release and says so, and the runbook says to build the first submission from a tag.
The publish guard read `github.event_name != 'workflow_dispatch'`, but inside a reusable workflow that is the CALLER's event. The packaging check calls publish-msix on a push, so the guard evaluated to "publish" and `workflow_call` had no input to say otherwise: once the Store credentials exist, any push touching packaging/** would have submitted a branch build to Partner Center. Callers now opt in through a `publish` input that defaults to false, and only tag-release passes true. The version-staleness step gates the guard as well instead of printing a warning the following steps ignored — a submission below the latest release cannot be withdrawn, so refusing is the only safe answer.
`github.ref` is the caller's ref inside a reusable workflow, and tag-release runs on a push to main rather than on the tag, so a release and a packaging check both landed in publish-msix-refs/heads/main and queued behind each other — the contention the group was split to avoid. `inputs.ref` is the ref the run actually builds, which does tell the two apart.
`workspaces: 'src-tauri -> target'` points at src-tauri/target, which does not exist: the [workspace] table lives in the repository root Cargo.toml, so cargo writes to ./target. Nothing was ever cached, and every Windows MSIX build paid for a cold compile from scratch. ci.yml already had this right.
makeappx falls back to the current date-time when `bundle` is called without /bv, so the bundle built by the packaging check carried Identity Version="2026.819.1209.0" while the packages inside it were 0.25.0.0. Store versions only ever move forward: submitting one bundle at a date-derived version would have locked the listing out of every semantic version for good. The version now comes from a shared Get-MsixVersion, so the package and the bundle cannot drift apart.
…king Only the exact file about to be written was removed. bundle-msix.ps1 globs *.msix, so in the local build-x64 / build-arm64 / bundle flow a package left over from before a version bump was staged alongside the new one and makeappx saw two packages for the same architecture.
The tag fell back to a hardcoded v0.27.0, so the job would have kept installing an old release forever — and an asset renamed by a later release, which is the one regression this job exists to catch, would have sailed past it. Resolve the latest tag instead.
The listing screenshots were deleted up front and each source was only checked as it was copied, so a renamed capture in the docs repo left the directory holding part of the old set and part of the new, recoverable only from git.
flatpak-external-data-checker rewrites the source url and sha256 and nothing else, so the <releases> block the store page reads its version from stays where the submission left it. Both the manifest generator and the runbook claimed nothing had to run again after the first merge.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The release already automated Homebrew (own tap), winget and the signed apt/yum repos. What was missing were the channels users actually browse — and the Microsoft Store, which is now free to publish to and needs no code-signing certificate on the MSIX path.
Channels added
voltius-bin)publish-installers.yml, every releasepublish-msix.yml, every releaseScoop and Flathub carry
checkver/x-checker-data, so each ecosystem's own bot follows releases once the manifest is merged — no job here is needed for them.Bugs found while packaging
.deblinkedlibsecret-1.so.0without declaring it. On a minimal Debian the app fails to start with a missing-so error. Declared for deb and rpm..desktophad an emptyCategories=, leaving the app uncategorised in every application menu. Setbundle.category.C:\Program Files\WindowsApps, which is not writable, so the in-app updater would have downloaded an update and then failed at the install step.classify_installnow reports that path as externally updated, with a unit test. Flatpak needed no change: noAPPIMAGEenv already meansExternal.Verification
cargo test --lib updater_tests— 8 passed, including the new MSIX case.makepkginarchlinux:base-devel— the PKGBUILD builds; the package containsusr/bin/voltius, the desktop file and all three icons. The generated sha256 matches the released.deb.flatpak-builder-lint manifest— clean apart from three errors that need Flathub exceptions (below). Runtime bumped 48 → 50 after the linter flagged 48 EOL.appstreamcli validateanddesktop-file-validate— both pass.ScoopInstaller/Scoopschema.json.Known gaps
/Sand assume%LOCALAPPDATA%\Voltius. They have never run on a real Windows machine. Test before opening the Extras PR.bundle.categoryactually populates the Linux.desktopCategories=needs a realtauri build. Worst case it stays empty, as today.Flathub exceptions
Three linter errors are expected and each needs a justification in the submission PR:
--filesystem=home(SFTP needs the local side of a transfer),--socket=ssh-auth(agent forwarding),--talk-name=org.freedesktop.Flatpak(the local-terminal feature opens the user's real shell; without host spawn it silently becomes a shell inside the sandbox). Written out inpackaging/README.md.The Flatpak app id is
app.voltius.Voltius, matching the domain Flathub verifies against, rather than the Tauri identifiercom.voltius.app.Manual setup still required
Documented per channel in
packaging/README.md: an AUR account +AUR_SSH_PRIVATE_KEY, the Scoop PR, the Flathub PR, and — for the Store — reserving the name in Partner Center and completing one submission by hand, because the Store CLI can only update a listing that is already live.